/**
 * Copyright (c) 2008, EffCode, www.effcode.com
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 * 
 * Redistributions of source code must retain the above copyright notice, this list 
 * of conditions and the following disclaimer. 
 * Redistributions in binary form must reproduce the above copyright notice, this list 
 * of conditions and the following disclaimer in the documentation and/or other materials
 * provided with the distribution. 
 * Neither the name of the EffCode nor the names of its contributors may be used 
 * to endorse or promote products derived from this software without specific prior written 
 * permission. 
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 
 * THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 
 * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 
 * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.effdom.io.impl;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import org.effdom.Attr;
import org.effdom.Document;
import org.effdom.Element;
import org.effdom.DocumentFactory.CompressType;
import org.effdom.impl.AttrBase;
import org.effdom.impl.AttrBinary;
import org.effdom.impl.AttrNumber;
import org.effdom.impl.AttrString;
import org.effdom.impl.DocumentImpl;
import org.effdom.impl.ElementImpl;
import org.effdom.impl.NodeBase;
import org.effdom.io.DocumentReader;
import org.effdom.io.DocumentWriter;

/**
 * @author <a href="mailto:mattias@effcode.com">Mattias Jonsson</a>
 * 
 * Protocol: Byte 1: Version, Type (Element, ATTR_*) Byte 2 or 2 - 3: ID (1 or 2
 * bytes depending on ID value) if Type == Element or ATTR_STRING or ATTR_BINARY
 * Byte 4, 4 - 7: Length (1 or 4 bytes depending on size of length) Byte 8 => n:
 * Data If numbers Byte 4 - 7: Int value if Type = ATTR_INT Byte 4 - 11: Int
 * value if Type = ATTR_LONG
 * 
 * Protocol version 3 Byte 1 Bit 6-7: Version Bit 5: ID size (0 => 1 byte, 1 =>
 * 2 bytes) Bit 4: Length (0 => 1 byte, 1 => 4 bytes) Only used for ELEMENT,
 * ATTR_STRING, ATTR_BINARY Bit 0-3: Type of node (ATTR_STRING, ATTR_BINARY,
 * ATT_LONG, ATTR_INT, ELEMENT)
 */
public class DocParserImpl implements DocumentReader, DocumentWriter {

    public static final byte VERSION = 0x10;

    public static int SIZE_LENGTH = 1;

    public static int SIZE_LENGTH_L = 4;

    public static int HEADER_LENGTH = 1;

    public static int ID_LENGTH_L = 2;

    public static int ID_LENGTH = 1;

    public static byte ID_SIZE_BIT = 0x20;

    public static byte LENGTH_SIZE_BIT = 0x10;

    public final static byte ATTR_STRING = 1;

    public final static byte ATTR_BINARY = 2;

    public final static byte ATTR_BYTE = 3;

    public final static byte ATTR_SHORT = 4;

    public final static byte ATTR_INT = 5;

    public final static byte ATTR_LONG = 6;

    public final static byte ELEMENT = 7;

    private static final byte ELEMENT_W_VALUE = 8;

    private static final byte ELEMENT_W_VALUE_L = 9;

    public static final int ATTR_HEADER_SIZE = 1;

    public static final int ELEM_HEADER_SIZE = 1;

    private static final int USE_ZIP = 0x01; // 0000 0001

    private static final int MIN_GZIP_LENGTH = 200;

    protected CompressType compressType;

    protected boolean useTar;

    public DocParserImpl(CompressType compressType) {
        this.compressType = compressType;
    }

    public Document read(InputStream stream) throws IOException, IllegalArgumentException {
        DocumentImpl doc = new DocumentImpl();
        DataInputStream in = new DataInputStream(stream);
        int packetHeader = in.readByte();
        if (VERSION == (packetHeader & 0xf0)) { // 1111 0000
            int compressed = packetHeader & 0x03; // 0000 0011
            if (USE_ZIP == compressed) {
                GZIPInputStream zin = new GZIPInputStream(stream);
                in = new DataInputStream(zin);
            }
            int header = in.readByte();
            short id = 0;
            int hSize = header & ID_SIZE_BIT; // 0010 0000
            if (hSize == 0) {
                id = in.readByte();
            } else {
                id = in.readShort();
            }
            hSize = header & LENGTH_SIZE_BIT; // 0001 0000
            int length = 0;
            if (hSize == 0) {
                length = in.readByte();
            } else {
                length = in.readInt();
            }
            ElementImpl elem = doc.createElement(null, id);
            
            byte type = (byte) (header & 0x0f); // 0000 1111
            if (type == ELEMENT_W_VALUE || type == ELEMENT_W_VALUE_L) {
                int lengthValue = 0;
                if (type == ELEMENT_W_VALUE) {
                    lengthValue = in.readByte();
                    length -= SIZE_LENGTH;
                } else {
                    lengthValue = in.readInt();
                    length -= SIZE_LENGTH_L;
                }
                byte[] strValue = new byte[lengthValue];
                in.readFully(strValue);
                length -= lengthValue;
                ByteArrayInputStream in2 = new ByteArrayInputStream(strValue);
            }

            doc.setRootElement(elem);
            read(in, doc, elem, length);
        } else {
            throw new IllegalArgumentException("Got wrong protocol version, expected '" + VERSION + "'. Got version '"
                    + (packetHeader & 0xc0) + "'.");
        }
        return doc;
    }

    private void read(DataInputStream in, DocumentImpl doc, ElementImpl elem, int index) throws IOException, IllegalArgumentException {
        try {

            while (index > 0) {
                int header = in.readByte();
                index -= HEADER_LENGTH;
                byte type = (byte) (header & 0x0f); // 0000 1111
                short id = 0;
                int hSize = header & ID_SIZE_BIT; // 0010 0000
                if (hSize == 0) {
                    id = in.readByte();
                    index -= ID_LENGTH;
                } else {
                    id = in.readShort();
                    index -= ID_LENGTH_L;
                }

                switch (type) {
                case ELEMENT:
                case ELEMENT_W_VALUE:
                case ELEMENT_W_VALUE_L:
                    hSize = header & LENGTH_SIZE_BIT; // 0001 0000
                    int length = 0;
                    if (hSize == 0) {
                        length = in.readByte();
                        index -= SIZE_LENGTH;
                    } else {
                        length = in.readInt();
                        index -= SIZE_LENGTH_L;
                    }
                    ElementImpl subElem = (ElementImpl) elem.element(id);

                    if (type == ELEMENT_W_VALUE || type == ELEMENT_W_VALUE_L) {
                        int tmp = 0;
                        int lengthValue = 0;
                        if (type == ELEMENT_W_VALUE) {
                            lengthValue = in.readByte();
                            tmp += SIZE_LENGTH;
                        } else {
                            lengthValue = in.readInt();
                            tmp += SIZE_LENGTH_L;
                        }
                        length -= tmp;
                        index -= tmp;
                    }

                    read(in, doc, subElem, length);
                    index -= length;
                    break;
                case ATTR_BYTE:
                    byte byteValue = in.readByte();
                    index -= 1;
                    elem.attr(id, byteValue);
                    break;
                case ATTR_SHORT:
                    short shortValue = in.readShort();
                    index -= 2;
                    elem.attr(id, shortValue);
                    break;
                case ATTR_INT:
                    int intValue = in.readInt();
                    index -= 4;
                    elem.attr(id, intValue);
                    break;
                case ATTR_LONG:
                    long longValue = in.readLong();
                    index -= 8;
                    elem.attr(id, longValue);
                    break;
                case ATTR_BINARY:
                    hSize = header & LENGTH_SIZE_BIT; // 0001 0000
                    length = 0;
                    if (hSize == 0) {
                        length = in.readByte();
                        index -= SIZE_LENGTH;
                    } else {
                        length = in.readInt();
                        index -= SIZE_LENGTH_L;
                    }
                    byte[] binValue = new byte[length];
                    in.readFully(binValue);
                    index -= length;
                    elem.attr(id, binValue);
                    break;
                case ATTR_STRING:
                    hSize = header & LENGTH_SIZE_BIT; // 0001 0000
                    length = 0;
                    if (hSize == 0) {
                        length = in.readByte();
                        index -= SIZE_LENGTH;
                    } else {
                        length = in.readInt();
                        index -= SIZE_LENGTH_L;
                    }
                    byte[] strValue = new byte[length];
                    in.readFully(strValue);
                    index -= length;
                    ByteArrayInputStream in2 = new ByteArrayInputStream(strValue);
                    DataInputStream in3 = new DataInputStream(in2);
                    elem.attr(id, in3.readUTF());
                    // System.out.println("read: string create: " + id + ", len:
                    // " + length + ", HexValue: " + Hex.toHex(strValue));
                    // System.out.println("read: string create: " + id + ", len:
                    // " + length + ", Value: " + new String(strValue));
                    break;
                default:
                    throw new IllegalArgumentException("Not a valid type of data: " + type);
                }
            }
        } catch (Exception e) {
            throw new IOException("Got unexcepted exception during parsing", e);
        }
    }

    public void write(Document doc, OutputStream stream) throws IOException, IllegalArgumentException {
        if (doc.rootElement() == null) {
            throw new IllegalArgumentException("No Document root element is set");
        }
        DataOutputStream out = null;
        ByteArrayOutputStream arrayOut = null;
        byte packetHeader = VERSION;
        if (compressType == CompressType.GZIP && doc.rootElement().length() > MIN_GZIP_LENGTH) {
            packetHeader = (byte) (packetHeader | USE_ZIP);
            arrayOut = new ByteArrayOutputStream();
            out = new DataOutputStream(arrayOut);
            out.write(packetHeader);
            GZIPOutputStream zout = new GZIPOutputStream(out);
            out = new DataOutputStream(zout);
        } else {
            out = new DataOutputStream(stream);
            out.writeByte(packetHeader);
        }
        write(out, (ElementImpl) doc.rootElement(), true);
        if (compressType == CompressType.GZIP && doc.rootElement().length() > MIN_GZIP_LENGTH) {
            out.flush();
            out.close();
            stream.write(arrayOut.toByteArray());
        } else {
            out.flush();
        }
    }

    private void write(DataOutputStream out, ElementImpl elem, boolean isRoot) throws IOException, IllegalArgumentException {
        byte header = ELEMENT;
        header = writeHeaderL(header, elem, out);

        if ((header & LENGTH_SIZE_BIT) == 0) {
            out.writeByte((byte) elem.length());
        } else {
            out.writeInt(elem.length());
        }

        if (elem.hasAttr()) {
            for (Attr a : elem.attrs()) {
                AttrBase attr = (AttrBase) a;
                if (attr instanceof AttrNumber) {
                    if (isByte(attr.valueAsLong())) {
                        header = ATTR_BYTE;
                        header = writeHeader(header, attr, out);
                        out.writeByte((byte) attr.valueAsInt());
                    } else if (isShort(attr.valueAsLong())) {
                        header = ATTR_SHORT;
                        header = writeHeader(header, attr, out);
                        out.writeShort((short) attr.valueAsInt());
                    } else if (isInt(attr.valueAsLong())) {
                        header = ATTR_INT;
                        header = writeHeader(header, attr, out);
                        out.writeInt(attr.valueAsInt());
                    } else {
                        header = ATTR_LONG;
                        header = writeHeader(header, attr, out);
                        out.writeLong(attr.valueAsLong());
                    }
                } else if (attr instanceof AttrBinary) {
                    header = ATTR_BINARY;
                    header = writeHeaderL(header, attr, out);

                    if ((header & LENGTH_SIZE_BIT) == 0) {
                        out.writeByte((byte) attr.length());
                    } else {
                        out.writeInt(attr.length());
                    }

                    out.write(attr.valueAsByteArray());
                } else if (attr instanceof AttrString) {
                    header = ATTR_STRING;
                    header = writeHeaderL(header, attr, out);

                    if ((header & LENGTH_SIZE_BIT) == 0) {
                        out.writeByte((byte) attr.length());
                    } else {
                        out.writeInt(attr.length());
                    }

                    out.write(attr.valueAsByteArray());
                }
            }
        }
        if (elem.hasElements()) {
            for (Element element : elem.elements()) {
                write(out, (ElementImpl) element, false);
            }
        }
    }

    private byte writeHeader(byte header, NodeBase attr, DataOutputStream out) throws IOException {
        if (!isByte(attr.id())) {
            header = (byte) (header | ID_SIZE_BIT);
        }
        out.writeByte(header);

        if ((header & ID_SIZE_BIT) == 0) {
            out.writeByte((byte) attr.id());
        } else {
            out.writeShort(attr.id());
        }
        return header;
    }

    private byte writeHeaderL(byte header, NodeBase attr, DataOutputStream out) throws IOException {
        if (!isByte(attr.id())) {
            header = (byte) (header | ID_SIZE_BIT);
        }
        if (attr.length() > Byte.MAX_VALUE) {
            header = (byte) (header | LENGTH_SIZE_BIT);
        }
        out.writeByte(header);

        if ((header & ID_SIZE_BIT) == 0) {
            out.writeByte((byte) attr.id());
        } else {
            out.writeShort(attr.id());
        }
        return header;
    }

    private boolean isInt(long l) {
        return l <= Integer.MAX_VALUE && l >= Integer.MIN_VALUE;
    }

    private boolean isShort(long l) {
        return l <= Short.MAX_VALUE && l >= Short.MIN_VALUE;
    }

    private boolean isByte(long l) {
        return l <= Byte.MAX_VALUE && l >= Byte.MIN_VALUE;
    }

}
